import { ApproximateTerrainHeights } from '../../Source/Cesium.js';
import { ArcType } from '../../Source/Cesium.js';
import { arraySlice } from '../../Source/Cesium.js';
import { Cartesian3 } from '../../Source/Cesium.js';
import { Cartographic } from '../../Source/Cesium.js';
import { Ellipsoid } from '../../Source/Cesium.js';
import { GeographicProjection } from '../../Source/Cesium.js';
import { GroundPolylineGeometry } from '../../Source/Cesium.js';
import { Math as CesiumMath } from '../../Source/Cesium.js';
import { WebMercatorProjection } from '../../Source/Cesium.js';
import createPackableSpecs from '../createPackableSpecs.js';

describe('Core/GroundPolylineGeometry', function() {

    beforeAll(function() {
        return ApproximateTerrainHeights.initialize();
    });

    afterAll(function() {
        ApproximateTerrainHeights._initPromise = undefined;
        ApproximateTerrainHeights._terrainHeights = undefined;
    });

    function verifyAttributeValuesIdentical(attribute) {
        var values = attribute.values;
        var componentsPerAttribute = attribute.componentsPerAttribute;
        var vertexCount = values.length / componentsPerAttribute;
        var firstVertex = arraySlice(values, 0, componentsPerAttribute);
        var identical = true;
        for (var i = 1; i < vertexCount; i++) {
            var index = i * componentsPerAttribute;
            var vertex = arraySlice(values, index, index + componentsPerAttribute);
            for (var j = 0; j < componentsPerAttribute; j++) {
                if (vertex[j] !== firstVertex[j]) {
                    identical = false;
                    break;
                }
            }
        }
        expect(identical).toBe(true);
    }

    it('computes positions and additional attributes for polylines', function() {
        var startCartographic = Cartographic.fromDegrees(0.01, 0.0);
        var endCartographic = Cartographic.fromDegrees(0.02, 0.0);
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromRadiansArray([
                startCartographic.longitude, startCartographic.latitude,
                endCartographic.longitude, endCartographic.latitude
            ]),
            granularity : 0.0
        });

        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry.indices.length).toEqual(36);
        expect(geometry.attributes.position.values.length).toEqual(24);

        var startHiAndForwardOffsetX = geometry.attributes.startHiAndForwardOffsetX;
        var startLoAndForwardOffsetY = geometry.attributes.startLoAndForwardOffsetY;
        var startNormalAndForwardOffsetZ = geometry.attributes.startNormalAndForwardOffsetZ;
        var endNormalAndTextureCoordinateNormalizationX = geometry.attributes.endNormalAndTextureCoordinateNormalizationX;
        var rightNormalAndTextureCoordinateNormalizationY = geometry.attributes.rightNormalAndTextureCoordinateNormalizationY;
        var startHiLo2D = geometry.attributes.startHiLo2D;
        var offsetAndRight2D = geometry.attributes.offsetAndRight2D;
        var startEndNormals2D = geometry.attributes.startEndNormals2D;
        var texcoordNormalization2D = geometry.attributes.texcoordNormalization2D;

        // Expect each entry in the additional attributes to be identical across all vertices since this is a single segment,
        // except endNormalAndTextureCoordinateNormalizationX and texcoordNormalization2D, which should be "sided"
        verifyAttributeValuesIdentical(startHiAndForwardOffsetX);
        verifyAttributeValuesIdentical(startLoAndForwardOffsetY);
        verifyAttributeValuesIdentical(startNormalAndForwardOffsetZ);
        verifyAttributeValuesIdentical(startHiLo2D);
        verifyAttributeValuesIdentical(offsetAndRight2D);
        verifyAttributeValuesIdentical(startEndNormals2D);

        // Expect endNormalAndTextureCoordinateNormalizationX and texcoordNormalization2D.x to encode the "side" of the geometry
        var i;
        var index;
        var values = endNormalAndTextureCoordinateNormalizationX.values;
        for (i = 0; i < 4; i++) {
            index = i * 4 + 3;
            expect(CesiumMath.sign(values[index])).toEqual(1.0);
        }
        for (i = 4; i < 8; i++) {
            index = i * 4 + 3;
            expect(CesiumMath.sign(values[index])).toEqual(-1.0);
        }

        values = texcoordNormalization2D.values;
        for (i = 0; i < 4; i++) {
            index = i * 2;
            expect(CesiumMath.sign(values[index])).toEqual(1.0);
        }
        for (i = 4; i < 8; i++) {
            index = i * 2;
            expect(CesiumMath.sign(values[index])).toEqual(-1.0);
        }

        // Expect rightNormalAndTextureCoordinateNormalizationY and texcoordNormalization2D.y to encode if the vertex is on the bottom
        values = rightNormalAndTextureCoordinateNormalizationY.values;
        expect(values[3]).toBeGreaterThan(1.0);
        expect(values[1 * 4 + 3]).toBeGreaterThan(1.0);
        expect(values[4 * 4 + 3]).toBeGreaterThan(1.0);
        expect(values[5 * 4 + 3]).toBeGreaterThan(1.0);

        values = texcoordNormalization2D.values;
        expect(values[1]).toBeGreaterThan(1.0);
        expect(values[1 * 2 + 1]).toBeGreaterThan(1.0);
        expect(values[4 * 2 + 1]).toBeGreaterThan(1.0);
        expect(values[5 * 2 + 1]).toBeGreaterThan(1.0);

        // Line segment geometry is encoded as:
        // - start position
        // - offset to the end position
        // - normal for a mitered plane at each end
        // - a right-facing normal
        // - parameters for localizing the position along the line to texture coordinates
        var startPosition3D = new Cartesian3();
        startPosition3D.x = startHiAndForwardOffsetX.values[0] + startLoAndForwardOffsetY.values[0];
        startPosition3D.y = startHiAndForwardOffsetX.values[1] + startLoAndForwardOffsetY.values[1];
        startPosition3D.z = startHiAndForwardOffsetX.values[2] + startLoAndForwardOffsetY.values[2];
        var reconstructedCarto = Cartographic.fromCartesian(startPosition3D);
        reconstructedCarto.height = 0.0;
        expect(Cartographic.equalsEpsilon(reconstructedCarto, startCartographic, CesiumMath.EPSILON7)).toBe(true);

        var endPosition3D = new Cartesian3();
        endPosition3D.x = startPosition3D.x + startHiAndForwardOffsetX.values[3];
        endPosition3D.y = startPosition3D.y + startLoAndForwardOffsetY.values[3];
        endPosition3D.z = startPosition3D.z + startNormalAndForwardOffsetZ.values[3];
        reconstructedCarto = Cartographic.fromCartesian(endPosition3D);
        reconstructedCarto.height = 0.0;
        expect(Cartographic.equalsEpsilon(reconstructedCarto, endCartographic, CesiumMath.EPSILON7)).toBe(true);

        var startNormal3D = Cartesian3.unpack(startNormalAndForwardOffsetZ.values);
        expect(Cartesian3.equalsEpsilon(startNormal3D, new Cartesian3(0.0, 1.0, 0.0), CesiumMath.EPSILON2)).toBe(true);

        var endNormal3D = Cartesian3.unpack(endNormalAndTextureCoordinateNormalizationX.values);
        expect(Cartesian3.equalsEpsilon(endNormal3D, new Cartesian3(0.0, -1.0, 0.0), CesiumMath.EPSILON2)).toBe(true);

        var rightNormal3D = Cartesian3.unpack(rightNormalAndTextureCoordinateNormalizationY.values);
        expect(Cartesian3.equalsEpsilon(rightNormal3D, new Cartesian3(0.0, 0.0, -1.0), CesiumMath.EPSILON2)).toBe(true);

        var texcoordNormalizationX = endNormalAndTextureCoordinateNormalizationX.values[3];
        expect(texcoordNormalizationX).toEqualEpsilon(1.0, CesiumMath.EPSILON3);

        // 2D
        var projection = new GeographicProjection();

        var startPosition2D = new Cartesian3();
        startPosition2D.x = startHiLo2D.values[0] + startHiLo2D.values[2];
        startPosition2D.y = startHiLo2D.values[1] + startHiLo2D.values[3];
        reconstructedCarto = projection.unproject(startPosition2D);
        reconstructedCarto.height = 0.0;
        expect(Cartographic.equalsEpsilon(reconstructedCarto, startCartographic, CesiumMath.EPSILON7)).toBe(true);

        var endPosition2D = new Cartesian3();
        endPosition2D.x = startPosition2D.x + offsetAndRight2D.values[0];
        endPosition2D.y = startPosition2D.y + offsetAndRight2D.values[1];
        reconstructedCarto = projection.unproject(endPosition2D);
        reconstructedCarto.height = 0.0;
        expect(Cartographic.equalsEpsilon(reconstructedCarto, endCartographic, CesiumMath.EPSILON7)).toBe(true);

        var startNormal2D = new Cartesian3();
        startNormal2D.x = startEndNormals2D.values[0];
        startNormal2D.y = startEndNormals2D.values[1];
        expect(Cartesian3.equalsEpsilon(startNormal2D, new Cartesian3(1.0, 0.0, 0.0), CesiumMath.EPSILON2)).toBe(true);

        var endNormal2D = new Cartesian3();
        endNormal2D.x = startEndNormals2D.values[2];
        endNormal2D.y = startEndNormals2D.values[3];
        expect(Cartesian3.equalsEpsilon(endNormal2D, new Cartesian3(-1.0, 0.0, 0.0), CesiumMath.EPSILON2)).toBe(true);

        var rightNormal2D = new Cartesian3();
        rightNormal2D.x = offsetAndRight2D.values[2];
        rightNormal2D.y = offsetAndRight2D.values[3];
        expect(Cartesian3.equalsEpsilon(rightNormal2D, new Cartesian3(0.0, -1.0, 0.0), CesiumMath.EPSILON2)).toBe(true);

        texcoordNormalizationX = texcoordNormalization2D.values[0];
        expect(texcoordNormalizationX).toEqualEpsilon(1.0, CesiumMath.EPSILON3);
    });

    it('does not generate 2D attributes when scene3DOnly is true', function() {
        var startCartographic = Cartographic.fromDegrees(0.01, 0.0);
        var endCartographic = Cartographic.fromDegrees(0.02, 0.0);
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromRadiansArray([
                startCartographic.longitude, startCartographic.latitude,
                endCartographic.longitude, endCartographic.latitude
            ]),
            granularity : 0.0
        });

        groundPolylineGeometry._scene3DOnly = true;

        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry.attributes.startHiAndForwardOffsetX).toBeDefined();
        expect(geometry.attributes.startLoAndForwardOffsetY).toBeDefined();
        expect(geometry.attributes.startNormalAndForwardOffsetZ).toBeDefined();
        expect(geometry.attributes.endNormalAndTextureCoordinateNormalizationX).toBeDefined();
        expect(geometry.attributes.rightNormalAndTextureCoordinateNormalizationY).toBeDefined();

        expect(geometry.attributes.startHiLo2D).not.toBeDefined();
        expect(geometry.attributes.offsetAndRight2D).not.toBeDefined();
        expect(geometry.attributes.startEndNormals2D).not.toBeDefined();
        expect(geometry.attributes.texcoordNormalization2D).not.toBeDefined();
    });

    it('removes adjacent positions with the same latitude/longitude', function() {
        var startCartographic = Cartographic.fromDegrees(0.01, 0.0);
        var endCartographic = Cartographic.fromDegrees(0.02, 0.0);
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromRadiansArrayHeights([
                startCartographic.longitude, startCartographic.latitude, 0.0,
                endCartographic.longitude, endCartographic.latitude, 0.0,
                endCartographic.longitude, endCartographic.latitude, 0.0,
                endCartographic.longitude, endCartographic.latitude, 10.0
            ]),
            granularity : 0.0
        });

        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry.indices.length).toEqual(36);
        expect(geometry.attributes.position.values.length).toEqual(24);
    });

    it('returns undefined if filtered points are not a valid geometry', function() {
        var startCartographic = Cartographic.fromDegrees(0.01, 0.0);
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromRadiansArrayHeights([
                startCartographic.longitude, startCartographic.latitude, 0.0,
                startCartographic.longitude, startCartographic.latitude, 0.0
            ]),
            granularity : 0.0
        });

        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry).toBeUndefined();
    });

    it('miters turns', function() {
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                0.01, 0.0,
                0.02, 0.0,
                0.02, 0.01
            ]),
            granularity : 0.0
        });

        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
        expect(geometry.indices.length).toEqual(72);
        expect(geometry.attributes.position.values.length).toEqual(48);

        var startNormalAndForwardOffsetZvalues = geometry.attributes.startNormalAndForwardOffsetZ.values;
        var endNormalAndTextureCoordinateNormalizationXvalues = geometry.attributes.endNormalAndTextureCoordinateNormalizationX.values;

        var miteredStartNormal = Cartesian3.unpack(startNormalAndForwardOffsetZvalues, 32);
        var miteredEndNormal = Cartesian3.unpack(endNormalAndTextureCoordinateNormalizationXvalues, 0);
        var reverseMiteredEndNormal = Cartesian3.multiplyByScalar(miteredEndNormal, -1.0, new Cartesian3());

        expect(Cartesian3.equalsEpsilon(miteredStartNormal, reverseMiteredEndNormal, CesiumMath.EPSILON7)).toBe(true);

        var approximateExpectedMiterNormal = new Cartesian3(0.0, 1.0, 1.0);
        Cartesian3.normalize(approximateExpectedMiterNormal, approximateExpectedMiterNormal);
        expect(Cartesian3.equalsEpsilon(approximateExpectedMiterNormal, miteredStartNormal, CesiumMath.EPSILON2)).toBe(true);
    });

    it('breaks miters for tight turns', function() {
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                0.01, 0.0,
                0.02, 0.0,
                0.01, CesiumMath.EPSILON7
            ]),
            granularity : 0.0
        });

        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        var startNormalAndForwardOffsetZvalues = geometry.attributes.startNormalAndForwardOffsetZ.values;
        var endNormalAndTextureCoordinateNormalizationXvalues = geometry.attributes.endNormalAndTextureCoordinateNormalizationX.values;

        var miteredStartNormal = Cartesian3.unpack(startNormalAndForwardOffsetZvalues, 32);
        var miteredEndNormal = Cartesian3.unpack(endNormalAndTextureCoordinateNormalizationXvalues, 0);

        expect(Cartesian3.equalsEpsilon(miteredStartNormal, miteredEndNormal, CesiumMath.EPSILON7)).toBe(true);

        var approximateExpectedMiterNormal = new Cartesian3(0.0, -1.0, 0.0);

        Cartesian3.normalize(approximateExpectedMiterNormal, approximateExpectedMiterNormal);
        expect(Cartesian3.equalsEpsilon(approximateExpectedMiterNormal, miteredStartNormal, CesiumMath.EPSILON2)).toBe(true);

        // Break miter on loop end
        groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                0.01, 0.0,
                0.02, 0.0,
                0.015, CesiumMath.EPSILON7
            ]),
            granularity : 0.0,
            loop : true
        });

        geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        startNormalAndForwardOffsetZvalues = geometry.attributes.startNormalAndForwardOffsetZ.values;
        endNormalAndTextureCoordinateNormalizationXvalues = geometry.attributes.endNormalAndTextureCoordinateNormalizationX.values;

        // Check normals at loop end
        miteredStartNormal = Cartesian3.unpack(startNormalAndForwardOffsetZvalues, 0);
        miteredEndNormal = Cartesian3.unpack(endNormalAndTextureCoordinateNormalizationXvalues, 32 * 2);

        expect(Cartesian3.equalsEpsilon(miteredStartNormal, miteredEndNormal, CesiumMath.EPSILON7)).toBe(true);

        approximateExpectedMiterNormal = new Cartesian3(0.0, 1.0, 0.0);

        Cartesian3.normalize(approximateExpectedMiterNormal, approximateExpectedMiterNormal);
        expect(Cartesian3.equalsEpsilon(approximateExpectedMiterNormal, miteredStartNormal, CesiumMath.EPSILON2)).toBe(true);
    });

    it('interpolates long polyline segments', function() {
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                0.01, 0.0,
                0.02, 0.0
            ]),
            granularity : 600.0 // 0.01 to 0.02 is about 1113 meters with default ellipsoid, expect two segments
        });

        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry.indices.length).toEqual(72);
        expect(geometry.attributes.position.values.length).toEqual(48);

        // Interpolate one segment but not the other
        groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                0.01, 0.0,
                0.02, 0.0,
                0.0201, 0.0
            ]),
            granularity : 600.0
        });

        geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry.indices.length).toEqual(36 * 3);
        expect(geometry.attributes.position.values.length).toEqual(24 * 3);
    });

    it('interpolates long polyline segments for rhumb lines', function() {
        // rhumb distance = 289020, geodesic distance = 288677
        var positions = Cartesian3.fromDegreesArray([
            10, 75,
            20, 75
        ]);

        var rhumbGroundPolylineGeometry = new GroundPolylineGeometry({
            positions : positions,
            granularity : 2890.0,
            arcType: ArcType.RHUMB
        });
        var geodesicGroundPolylineGeometry = new GroundPolylineGeometry({
            positions : positions,
            granularity : 2890.0,
            arcType: ArcType.GEODESIC
        });

        var rhumbGeometry = GroundPolylineGeometry.createGeometry(rhumbGroundPolylineGeometry);
        var geodesicGeometry = GroundPolylineGeometry.createGeometry(geodesicGroundPolylineGeometry);

        expect(rhumbGeometry.indices.length).toEqual(3636);
        expect(geodesicGeometry.indices.length).toEqual(3600);
        expect(geodesicGeometry.attributes.position.values.length).toEqual(2400);
        expect(rhumbGeometry.attributes.position.values.length).toEqual(2424);

        // Interpolate one segment but not the other
        positions = Cartesian3.fromDegreesArray([
            10, 75,
            20, 75,
            20.01, 75
        ]);
        rhumbGroundPolylineGeometry = new GroundPolylineGeometry({
            positions : positions,
            granularity : 2890.0,
            arcType: ArcType.RHUMB
        });
        geodesicGroundPolylineGeometry = new GroundPolylineGeometry({
            positions : positions,
            granularity : 2890.0,
            arcType: ArcType.GEODESIC
        });

        rhumbGeometry = GroundPolylineGeometry.createGeometry(rhumbGroundPolylineGeometry);
        geodesicGeometry = GroundPolylineGeometry.createGeometry(geodesicGroundPolylineGeometry);

        expect(rhumbGeometry.indices.length).toEqual(3636 + 36);
        expect(geodesicGeometry.indices.length).toEqual(3600 + 36);
        expect(geodesicGeometry.attributes.position.values.length).toEqual(2400 + 24);
        expect(rhumbGeometry.attributes.position.values.length).toEqual(2424 + 24);
    });

    it('loops when there are enough positions and loop is specified', function() {
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                0.01, 0.0,
                0.02, 0.0
            ]),
            granularity : 0.0,
            loop : true
        });

        // Not enough positions to loop, should still be a single segment
        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
        expect(geometry.indices.length).toEqual(36);

        groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                0.01, 0.0,
                0.02, 0.0,
                0.02, 0.02
            ]),
            granularity : 0.0,
            loop : true
        });

        // Loop should produce 3 segments
        geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
        expect(geometry.indices.length).toEqual(108);
    });

    it('subdivides geometry across the IDL and Prime Meridian', function() {
        // Cross PM
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                -1.0, 0.0,
                1.0, 0.0
            ]),
            granularity : 0.0 // no interpolative subdivision
        });

        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry.indices.length).toEqual(72);
        expect(geometry.attributes.position.values.length).toEqual(48);

        // Cross IDL
        groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                -179.0, 0.0,
                179.0, 0.0
            ]),
            granularity : 0.0 // no interpolative subdivision
        });

        geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry.indices.length).toEqual(72);
        expect(geometry.attributes.position.values.length).toEqual(48);

        // Cross IDL going opposite direction and loop
        groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                179.0, 0.0,
                179.0, 1.0,
                -179.0, 1.0,
                -179.0, 0.0
            ]),
            granularity : 0.0, // no interpolative subdivision
            loop : true
        });

        geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry.indices.length).toEqual(6 * 36);
        expect(geometry.attributes.position.values.length).toEqual(6 * 24);

        // Near-IDL case
        groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                179.999, 80.0,
                -179.999, 80.0
            ]),
            granularity : 0.0 // no interpolative subdivision
        });

        geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        expect(geometry.indices.length).toEqual(72);
        expect(geometry.attributes.position.values.length).toEqual(48);
    });

    it('throws errors if not enough positions have been provided', function() {
        expect(function() {
            return new GroundPolylineGeometry({
                positions : Cartesian3.fromDegreesArray([
                    0.01, 0.0
                ]),
                granularity : 0.0,
                loop : true
            });
        }).toThrowDeveloperError();
    });

    it('can unpack onto an existing instance', function() {
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                -1.0, 0.0,
                1.0, 0.0
            ]),
            loop : true,
            granularity : 10.0 // no interpolative subdivision
        });
        groundPolylineGeometry._scene3DOnly = true;
        GroundPolylineGeometry.setProjectionAndEllipsoid(groundPolylineGeometry, new WebMercatorProjection(Ellipsoid.UNIT_SPHERE));

        var packedArray = [0];
        GroundPolylineGeometry.pack(groundPolylineGeometry, packedArray, 1);
        var scratch = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                -1.0, 0.0,
                1.0, 0.0
            ])
        });
        GroundPolylineGeometry.unpack(packedArray, 1, scratch);

        var scratchPositions = scratch._positions;
        expect(scratchPositions.length).toEqual(2);
        expect(Cartesian3.equals(scratchPositions[0], groundPolylineGeometry._positions[0])).toBe(true);
        expect(Cartesian3.equals(scratchPositions[1], groundPolylineGeometry._positions[1])).toBe(true);
        expect(scratch.loop).toBe(true);
        expect(scratch.granularity).toEqual(10.0);
        expect(scratch._ellipsoid.equals(Ellipsoid.UNIT_SPHERE)).toBe(true);
        expect(scratch._scene3DOnly).toBe(true);
        expect(scratch._projectionIndex).toEqual(1);
    });

    it('can unpack onto a new instance', function() {
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                -1.0, 0.0,
                1.0, 0.0
            ]),
            loop : true,
            granularity : 10.0 // no interpolative subdivision
        });
        groundPolylineGeometry._scene3DOnly = true;
        GroundPolylineGeometry.setProjectionAndEllipsoid(groundPolylineGeometry, new WebMercatorProjection(Ellipsoid.UNIT_SPHERE));

        var packedArray = [0];
        GroundPolylineGeometry.pack(groundPolylineGeometry, packedArray, 1);
        var result = GroundPolylineGeometry.unpack(packedArray, 1);

        var scratchPositions = result._positions;
        expect(scratchPositions.length).toEqual(2);
        expect(Cartesian3.equals(scratchPositions[0], groundPolylineGeometry._positions[0])).toBe(true);
        expect(Cartesian3.equals(scratchPositions[1], groundPolylineGeometry._positions[1])).toBe(true);
        expect(result.loop).toBe(true);
        expect(result.granularity).toEqual(10.0);
        expect(result._ellipsoid.equals(Ellipsoid.UNIT_SPHERE)).toBe(true);
        expect(result._scene3DOnly).toBe(true);
        expect(result._projectionIndex).toEqual(1);
    });

    it('provides a method for setting projection and ellipsoid', function() {
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : Cartesian3.fromDegreesArray([
                -1.0, 0.0,
                1.0, 0.0
            ]),
            loop : true,
            granularity : 10.0 // no interpolative subdivision
        });

        GroundPolylineGeometry.setProjectionAndEllipsoid(groundPolylineGeometry, new WebMercatorProjection(Ellipsoid.UNIT_SPHERE));

        expect(groundPolylineGeometry._projectionIndex).toEqual(1);
        expect(groundPolylineGeometry._ellipsoid.equals(Ellipsoid.UNIT_SPHERE)).toBe(true);
    });

    var positions = Cartesian3.fromDegreesArray([
        0.01, 0.0,
        0.02, 0.0,
        0.02, 0.1
    ]);
    var polyline = new GroundPolylineGeometry({
        positions : positions,
        granularity : 1000.0,
        loop : true
    });

    it('projects normals that cross the IDL', function() {
        var projection = new GeographicProjection();
        var cartographic = new Cartographic(CesiumMath.PI - CesiumMath.EPSILON11, 0.0);
        var normal = new Cartesian3(0.0, -1.0, 0.0);
        var projectedPosition = projection.project(cartographic, new Cartesian3());
        var result = new Cartesian3();

        GroundPolylineGeometry._projectNormal(projection, cartographic, normal, projectedPosition, result);
        expect(Cartesian3.equalsEpsilon(result, new Cartesian3(1.0, 0.0, 0.0), CesiumMath.EPSILON7)).toBe(true);
    });

    it('creates bounding spheres that cover the entire polyline volume height', function() {
        var positions = Cartesian3.fromDegreesArray([
            -122.17580380403314, 46.19984918190237,
            -122.17581380403314, 46.19984918190237
        ]);

        // Mt. St. Helens - provided coordinates are a few meters apart
        var groundPolylineGeometry = new GroundPolylineGeometry({
            positions : positions,
            granularity : 0.0 // no interpolative subdivision
        });

        var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);

        var boundingSphere = geometry.boundingSphere;
        var pointsDistance = Cartesian3.distance(positions[0], positions[1]);

        expect(boundingSphere.radius).toBeGreaterThan(pointsDistance);
        expect(boundingSphere.radius).toBeGreaterThan(1000.0); // starting top/bottom height
    });

    var packedInstance = [positions.length];
    Cartesian3.pack(positions[0], packedInstance, packedInstance.length);
    Cartesian3.pack(positions[1], packedInstance, packedInstance.length);
    Cartesian3.pack(positions[2], packedInstance, packedInstance.length);
    packedInstance.push(polyline.granularity);
    packedInstance.push(polyline.loop ? 1.0 : 0.0);
    packedInstance.push(polyline.arcType);

    Ellipsoid.pack(Ellipsoid.WGS84, packedInstance, packedInstance.length);

    packedInstance.push(0.0); // projection index for Geographic (default)
    packedInstance.push(0.0); // scene3DModeOnly = false

    createPackableSpecs(GroundPolylineGeometry, polyline, packedInstance);
});
